package service

import (
	"cap/daemon/gua"
	"cap/dao"
	"cap/global"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gorilla/websocket"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"gorm.io/gorm"
	"lib/id"
	"lib/jwt"
	"lib/structs/cap"
	"lib/structs/enum"
	"lib/structs/tables"
	"strconv"
	"strings"
	"time"
)

var SessionSer = new(sessionService)

type sessionService struct {
}

func (s sessionService) BindWinSessionId(sessionId int64, winSessionId int) (err error) {
	var sesInfo tables.CapSession
	sesInfo, err = dao.SessionDao.FindById(sessionId)
	if err != nil {
		return err
	}
	sesInfo.WinSessionId = winSessionId
	sesInfo.Status = tables.CapSessionStatusForHolding
	err = dao.SessionDao.UpdateById(sesInfo)
	return err
}
func (s sessionService) HandeSessionChange(changeMsg cap.WinSessionChangeMsg, hostId int64) (err error) {
	var hostInfo tables.CapHost
	hostInfo, err = dao.HostDao.FindById(hostId)
	if err != nil {
		return
	}
	switch changeMsg.WtsCode {
	case 1: // 本地连接
		logrus.Debugf("本地连接")
	case 2: // 本地断开
		logrus.Debugf("本地断开")
	case 3: // 远程连接
		logrus.Debugf("远程连接：%v", changeMsg)
	case 4: // 远程断开，正常不会触发，因为go会一直握持ws
		logrus.Debugf("远程断开：%v", changeMsg)
	case 5: // 会话登录
		logrus.Debugf("会话登录：%v", changeMsg)
	case 6: // 会话注销
		logrus.Debugf("会话注销：%v", changeMsg)
		var sesInfo tables.CapSession
		sesInfo, err = dao.SessionDao.FindByHostIdAndWinId(hostInfo.Id, changeMsg.WinSessionId)
		if err != nil {
			return
		}
		s.closeSessionById(sesInfo.Id, "会话注销", tables.CapSessionStatusForDestroyed)
	default:
		logrus.Errorf("WM_QUERYENDSESSION：不知道具体含义的状态，需要排查：%v", changeMsg)
	}
	return err
}

func (s sessionService) InsertOpenAppLogs(linkId, hostSessionId int64, winSessionId int) (err error) {
	var (
		linkInfo tables.CapLink
		sesInfo  tables.CapSession
		cluInfo  tables.CapCluster
	)
	sesInfo, err = dao.SessionDao.FindById(hostSessionId)
	if err != nil {
		return err
	}
	linkInfo, err = dao.SessionDao.FindLinkById(linkId)
	if err != nil {
		return err
	}
	cluInfo, err = dao.HostDao.FindClusterByCluId(linkInfo.SourceId)
	if err != nil {
		return err
	}
	sesInfo.WinSessionId = winSessionId
	err = dao.SessionDao.UpdateById(sesInfo)
	if err != nil {
		return err
	}
	metaMap := make(map[string]string)
	err = json.Unmarshal([]byte(linkInfo.MetaData), &metaMap)
	if err != nil {
		return
	}
	sal := tables.CapSessionMapApp{
		Id:          id.GetSnowId(),
		HostId:      sesInfo.HostId,
		SessionId:   sesInfo.Id,
		AppId:       linkId,
		AppName:     metaMap["appName"],
		Icon:        metaMap["icon"],
		ClusterId:   cluInfo.Id,
		ClusterName: cluInfo.Name,
		CreatedAt:   time.Now().Unix(),
	}
	err = dao.DB.Model(&tables.CapSessionMapApp{}).Create(&sal).Error
	return err
}

func (s sessionService) GetCapAppInfo(linkId, sessionId int64) (vmApp cap.VmAppParams, err error) {
	var (
		linkInfo    tables.CapLink
		sessionInfo tables.CapSession
	)
	linkInfo, err = dao.SessionDao.FindLinkById(linkId)
	if err != nil {
		return
	}
	sessionInfo, err = dao.SessionDao.FindById(sessionId)
	if err != nil {
		return
	}
	metaMap := make(map[string]string)
	err = json.Unmarshal([]byte(linkInfo.MetaData), &metaMap)
	if err != nil {
		return
	}
	vmApp = cap.VmAppParams{
		Username: "",
		Password: "",
		Domain:   "",
		RunAs:    false,

		AppName:       metaMap["appName"],
		HostSessionId: sessionInfo.Id,
		LinkId:        linkInfo.Id,
		ExePath:       metaMap["exePath"],
		WorkDir:       metaMap["workSpace"],
		Params:        strings.Split(metaMap["appParams"], " "),
	}
	return
}
func (s sessionService) Conn(in cap.OpenLinkDto, ws *websocket.Conn) (err error) {
	var link tables.CapLink
	link, err = dao.SessionDao.FindLinkById(in.LinkId)
	if err != nil {
		return err
	}
	// 更新会话信息
	var ses tables.CapSession
	ses, err = dao.SessionDao.FindById(in.SessionId)
	if err != nil {
		return err
	}
	var conf *gua.Configuration
	switch link.LinkType {
	case "vnc":
		conf, err = s.getVncConfig(link)
	case "sh":
		conf, err = s.getShConfig(link, in.Width, in.Height, in.Dpi)
	case "rdp":
		conf, err = s.getRdpConfig(link, in.Width, in.Height, in.Dpi)
	case "cap":
		conf, err = s.getRdpConfig(link, in.Width, in.Height, in.Dpi)
		s.setRdpForCap(conf, link.UserId, ses)
	default:
		err = fmt.Errorf("还未支持的协议类型")
		return err
	}
	if err != nil {
		return err
	}

	var guacdTunnel *gua.Tunnel
	guacdTunnel, err = gua.NewTunnel(global.Conf.GuacdAddr, conf)
	if err != nil {
		logrus.Debugf("建立连接失败: [%v],config:[%v]", err.Error(), conf)
		s.disconnect(ws, gua.NewTunnelError, err.Error())
		return err
	}
	isExist := gua.GuaSessionManager.IsExist(in.SessionId)
	if isExist {
		logrus.Debugln("复用会话")
	} else {
		logrus.Debugln("新建会话，将会话纳入管理")
		// 将相关连接参数封装到 session对象中
		nextSession := &gua.Session{
			ID:          in.SessionId,
			Protocol:    link.LinkType,
			Mode:        enum.Guacd,
			WebSocket:   ws,
			GuacdTunnel: guacdTunnel,
		}
		// 将 session对象保存到全局对象中
		nextSession.Observer = gua.NewObserver(in.SessionId)
		gua.GuaSessionManager.Add <- nextSession
		// 同时开启一个协程，检测这个会话的状态变化
		go nextSession.Observer.Start()
	}
	// 创建guacamoleHandler ，它提供了两个协程，用来读写 ws 和 tunnel 中的数据
	guacamoleHandler := gua.NewGuacamoleHandler(ws, guacdTunnel)
	guacamoleHandler.Start()

	ses.ConnectedTime = time.Now().Unix()
	ses.Width = in.Width
	ses.Height = in.Height
	ses.Status = tables.CapSessionStatusForHolding
	ses.ConnectionId = guacdTunnel.UUID
	err = dao.SessionDao.UpdateById(ses)
	if err != nil {
		return err
	}
	// 开启循环，读取websocket中的信息并发送到tunnel中，消息不是直接发送，而是通过 channel
	// java中通过lock来实现有序发送，go中的channel消息默认就是有系的。
	// 如果ws或者tunnel中读取消息时发生错误，则其中一方已经断开，执行关闭会话的操作,即：关闭连接，修改数据库中的会话状态。
	for {
		var message []byte
		_, message, err = ws.ReadMessage()
		if err != nil {
			guacamoleHandler.Stop()
			s.closeSessionById(ses.Id, "前端断开", tables.CapSessionStatusForBreak)
			return nil
		}
		_, err = guacdTunnel.WriteAndFlush(message)
		if err != nil {
			s.closeSessionById(ses.Id, "后端断开", tables.CapSessionStatusForBreak)
			return nil
		}
		//if string(message) == "10.disconnect;" {
		//	// 不接受前端发送的销毁信号，rdp断开、相关的会话销毁只能由go监听到会话销毁后移除
		//	// 必须要周期性发送 ping信号，否则guacd会判定读超时，连接会在1s左右被移除
		//	go func() {
		//		for _ = range time.Tick(time.Second) {
		//			instruction := gua.NewInstruction("ping", fmt.Sprintf("%d", time.Now().Unix()))
		//			_, err = guacdTunnel.WriteAndFlush([]byte(instruction.String()))
		//			if err != nil {
		//				logrus.Error(err)
		//				return
		//			}
		//		}
		//
		//	}()
		//} else {
		//
		//}
	}
}

func (s sessionService) Create(linkId int64, me jwt.CustomClaims) (sessionId int64, err error) {
	var (
		link     tables.CapLink
		host     tables.CapHost
		cert     tables.CapCertify
		sessionM tables.CapSession
	)
	err = dao.DB.Where("id = ?", linkId).First(&link).Error
	if err != nil {
		return
	}
	if link.LinkType == "cap" {
		host, cert, err = s.CreateForCap(link, me.UserID)
		if err != nil {
			return
		}
	} else {
		err = dao.DB.Where("id = ?", link.SourceId).First(&host).Error
		if err != nil {
			return
		}
		err = dao.DB.Where("id = ?", link.CertifyId).First(&cert).Error
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				err = fmt.Errorf("凭证信息已经被移除，请重新分配！")
			}
			return
		}
	}
	sessionM = tables.CapSession{
		Id:               id.GetSnowId(),
		Protocol:         link.LinkType,
		HostId:           host.Id,
		HostIp:           host.Ip,
		ConnectPort:      host.GuaPort,
		ConnectionId:     "",
		CertifyId:        strconv.FormatInt(cert.Id, 10),
		WinSessionId:     0,
		Width:            "",
		Height:           "",
		Status:           tables.CapSessionStatusForInit,
		Message:          "",
		CreatedAt:        time.Now().Unix(),
		ConnectedTime:    0,
		DisconnectedTime: 0,
		CreatorId:        me.UserID,
		ClusterId:        host.ClusterId,
		Creator:          me.Nickname,
	}
	if link.LinkType == "cap" {
		sessionM.CertifyId = ""
	}
	err = dao.DB.Create(&sessionM).Error
	if err != nil {
		return
	}
	return sessionM.Id, nil
}

func (s sessionService) CreateForCap(link tables.CapLink, userId int64) (hostInfo tables.CapHost, cert tables.CapCertify, err error) {
	if link.LinkType != "cap" {
		err = fmt.Errorf("不是cap协议")
		return
	}
	var clu tables.CapCluster
	err = dao.DB.Model(&tables.CapCluster{}).Where("id = ?", link.SourceId).First(&clu).Error
	if err != nil {
		return
	}
	hostInfo, err = s.selectHost(link.MetaData, clu, userId)
	if err != nil {
		return
	}
	return
}

// getCertify 获取凭证信息
func (s sessionService) getCertify(userId int64) (account, pwd string) {
	if userId == 1 {
		account = "root"
		pwd = "qwer1234!"
		return
	}
	account = fmt.Sprintf("%d", userId)
	pwd = "qwer1234!"
	return
}

// cap 协议下的挑选主机
func (s sessionService) selectHost(linkMetaData string, clu tables.CapCluster, userId int64) (host tables.CapHost, err error) {
	switch clu.ClusterType {
	case tables.ClusterTypeForPrivate:
		host, err = s.selectPrivateHost(userId, clu.Id)
	case tables.ClusterTypeForPreempt:
		host, err = s.selectPreemptHost(userId, clu.Id)
	case tables.ClusterTypeForShare:
		host, err = s.selectShareHost(linkMetaData, clu.Id)
	}
	return
}

// selectPrivateHost 挑选私有主机（不考虑会话复用，外层自己检测），先检查用户在该集群下是否已有私有主机，没有就挑选一台没有会话的然后绑定给公户
func (s sessionService) selectPrivateHost(userId int64, clusterId int64) (host tables.CapHost, err error) {
	host, err = dao.SessionDao.FindPrivateHost(userId, clusterId)
	if err != nil {
		return
	}
	if host.Id != 0 {
		return
	}
	var li []tables.CapHost
	li, err = dao.SessionDao.SelectZeroSessionHost(clusterId)
	if err != nil {
		return
	}
	if len(li) == 0 {
		err = fmt.Errorf("没有空闲主机可以分配了")
		return
	}
	host = li[0]
	host.BindUserID = userId
	err = dao.HostDao.UpdateById(host)
	return host, err
}

// selectPreemptHost 挑选抢占主机（不考虑会话复用，外层自己检测）。从集群下挑选出一台没有任何活跃会话也没有被人绑定或者锁定的主机，命中后锁定该主机
func (s sessionService) selectPreemptHost(userId int64, clusterId int64) (host tables.CapHost, err error) {
	var li []tables.CapHost
	li, err = dao.SessionDao.SelectZeroSessionHost(clusterId)
	if err != nil {
		return
	}
	if len(li) == 0 {
		err = fmt.Errorf("没有空闲主机可以分配了")
		return
	}
	host = li[0]
	host.LockedUserID = userId
	err = dao.HostDao.UpdateById(host)
	return host, err
}

// selectShareHost 挑选共享主机（不考虑会话复用，外层自己检测），只考虑单例应用问题
func (s sessionService) selectShareHost(linkMetaData string, clusterId int64) (host tables.CapHost, err error) {
	var (
		hosts   = make([]tables.CapHost, 0)
		allHost []tables.CapHost
	)
	allHost, err = dao.HostDao.FindByClusterId(clusterId)
	if err != nil {
		return
	}
	isSingle := HostSer.getMetaData(linkMetaData, "isSingle") == "true"
	if isSingle {
		appName := HostSer.getMetaData(linkMetaData, "name")
		var kvMap map[int64]int64
		kvMap, err = dao.SessionDao.CountAppRunTimesByActionSession(appName, clusterId)
		if err != nil {
			return
		}
		for _, item := range allHost {
			if kvMap[item.Id] == 0 {
				hosts = append(hosts, item)
			}
		}
	} else {
		hosts = allHost
	}
	if len(hosts) == 0 {
		err = fmt.Errorf("没有空闲主机可以分配了")
		return
	}
	// 挑选一台负载最小的主机
	host = s.SelectMin(hosts)
	return
}

func (s sessionService) SelectMin(li []tables.CapHost) (m tables.CapHost) {
	return li[0]
}

func (s sessionService) Page(in cap.SearchSessionForm) (li []tables.CapSession, total int64, err error) {
	li, total, err = dao.SessionDao.Page(in)
	if err != nil {
		return
	}
	return li, total, nil
}

func (s sessionService) closeSessionById(sessionId int64, msg string, status int) {
	var (
		ses tables.CapSession
		err error
	)
	ses, err = dao.SessionDao.FindById(sessionId)
	if err != nil {
		logrus.Errorf("closeSessionById %s", err.Error())
		return
	}
	ses.Message = msg
	ses.DisconnectedTime = time.Now().Unix()
	ses.Status = status
	err = dao.SessionDao.UpdateById(ses)
	if err != nil {
		logrus.Errorf("closeSessionById %s", err.Error())
		return
	}
}

func (s sessionService) disconnect(ws *websocket.Conn, code int, reason string) {
	// guacd 无法处理中文字符，所以进行了base64编码。
	encodeReason := base64.StdEncoding.EncodeToString([]byte(reason))
	err := gua.NewInstruction("error", encodeReason, strconv.Itoa(code))
	_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
	disconnect := gua.NewInstruction("disconnect")
	_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
}

func (s sessionService) getShConfig(lin tables.CapLink, width, height, dpi string) (conf *gua.Configuration, err error) {
	var cer tables.CapCertify
	cer, err = dao.SessionDao.FindCertifyById(lin.CertifyId)
	if err != nil {
		return
	}
	var host tables.CapHost
	host, err = dao.HostDao.FindById(lin.SourceId)
	if err != nil {
		return
	}
	if dpi == "" {
		dpi = "90"
	}
	conf = gua.NewConfiguration()
	conf.Protocol = "ssh"
	conf.SetParameter("hostname", host.Ip)
	conf.SetParameter("port", strconv.Itoa(host.GuaPort))
	conf.SetParameter("width", width)
	conf.SetParameter("height", height)
	conf.SetParameter("dpi", dpi)
	conf.SetParameter("username", cer.Account)
	conf.SetParameter("password", cer.Password)
	return conf, nil
}

func (s sessionService) getVncConfig(lin tables.CapLink) (conf *gua.Configuration, err error) {
	var cer tables.CapCertify
	cer, err = dao.SessionDao.FindCertifyById(lin.CertifyId)
	if err != nil {
		return
	}
	var host tables.CapHost
	host, err = dao.HostDao.FindById(lin.SourceId)
	if err != nil {
		return
	}
	conf = gua.NewConfiguration()
	conf.Protocol = "vnc"
	conf.SetParameter("hostname", host.Ip)
	conf.SetParameter("port", strconv.Itoa(host.GuaPort))
	if cer.Account == "" { // vnc 不一定需要密码
		conf.SetParameter("username", cer.Account)
	}
	conf.SetParameter("password", cer.Password)
	return conf, nil
}

func (s sessionService) getRdpConfig(lin tables.CapLink, width, height, dpi string) (conf *gua.Configuration, err error) {
	var cer tables.CapCertify
	cer, err = dao.SessionDao.FindCertifyById(lin.CertifyId)
	if err != nil {
		return
	}
	var host tables.CapHost
	host, err = dao.HostDao.FindById(lin.SourceId)
	if err != nil {
		return
	}
	conf = gua.NewConfiguration()
	conf.Protocol = "rdp"
	if dpi == "" {
		dpi = "90"
	}
	conf.SetParameter(gua.Username, cer.Account)
	conf.SetParameter(gua.Password, cer.Password)
	conf.SetParameter(gua.Hostname, host.Ip)
	conf.SetParameter(gua.Port, strconv.Itoa(host.GuaPort))
	conf.SetParameter("width", width)
	conf.SetParameter("height", height)
	conf.SetParameter("dpi", dpi)
	conf.SetParameter(gua.Security, "any")
	conf.SetParameter(gua.IgnoreCert, "true")
	conf.SetParameter(gua.ResizeMethod, "reconnect")
	return conf, nil
}

// setRdpForCap cap的app模式和rdp高度同源，这里对特殊参数进行额外的修改
func (s sessionService) setRdpForCap(conf *gua.Configuration, uid int64, sessionInfo tables.CapSession) {
	conf.SetParameter(gua.StaticChannels, cap.LauncherVisualChannelName)
	conf.SetParameter(gua.RemoteApp, cap.LauncherPath)
	account, pwd := s.getCertify(uid)
	conf.SetParameter(gua.Username, account)
	conf.SetParameter(gua.Password, pwd)
	conf.SetParameter(gua.Hostname, sessionInfo.HostIp)
	conf.SetParameter(gua.Port, strconv.Itoa(sessionInfo.ConnectPort))
	return
}
